luci-app-lldpd: custom TLV handling
authorPaul Donald <[email protected]>
Tue, 1 Apr 2025 16:12:39 +0000 (18:12 +0200)
committerPaul Donald <[email protected]>
Mon, 7 Apr 2025 18:54:20 +0000 (20:54 +0200)
Depends on https://github.com/openwrt/openwrt/pull/14872

Custom TLV have been possible in lldpd; now the machinery is in openwrt
to use them. Extend GUI to enable their handling.

TLV apply either globally or to specific interfaces. The parsing of TLV
strings mirrors that of lldpd. For details see lldpd man page.

Signed-off-by: Paul Donald <[email protected]>
applications/luci-app-lldpd/htdocs/luci-static/resources/view/lldpd/config.js

index 32f18035bcb019a0c5174582691e28b63da7cb84..859ed567b284bfc8143b9975c8a96921f67f3494 100644 (file)
@@ -828,6 +828,167 @@ return L.view.extend({
                o.depends('enable_sonmp', '1');
        },
 
+       // -----------------------------------------------------------------------------------------
+       //
+       //   Extra TLV
+       //
+       // -----------------------------------------------------------------------------------------
+
+       /** @private */
+       populateExtraTLVOptions: function(s, tab, data) {
+               let o, s, ss, oo;
+
+               ///// Custom TLV
+               // # syntax: [add | replace] oui OUI subtype SUBTYPE [oui-info CONTENT]
+               /////
+
+               o = s.taboption(tab, form.SectionValue, '_tlv_', form.TableSection, 'custom-tlv', _('Custom TLV'), 
+                       _('TLV that lack network interfaces here are sent according to <code>interface pattern</code> (Network IO under Network Interfaces).') + '<br/>' +
+                       _('Default insert behaviour is add.'));
+               ss = o.subsection;
+               ss.anonymous = true;
+               ss.sortable = true;
+               ss.addremove = true;
+               ss.rowcolors = true;
+               ss.addbtntitle = _('Add Custom TLV', 'lldpd Custom TLV');
+               oo = ss.option(lldpd.CBIMultiIOSelect, 'ports',
+                       _('Network Interface(s)'));
+               data[3].forEach(nd => {
+                       oo.value(nd.getName());
+                       oo.value('!'+nd.getName());
+               });
+               oo.validate = validateioentries;
+
+               const validateHexBytes = (str, length) => str.split(',').length === length && str.split(',').every(byte => /^[0-9a-f]{2}$/i.test(byte));
+
+               function parse_custom_tlv_string(input) {
+                       //[add | replace] oui OUI subtype SUBTYPE [oui-info CONTENT]
+                       const pattern = /^(add|replace)?\s*oui\s+([0-9a-f]{2}(?:,[0-9a-f]{2})*)\s+subtype\s+([0-9a-f]{2})(?:\s+oui-info\s+([0-9a-f]{2}(?:,[0-9a-f]{2})*))?$/i;
+                       const match = input.match(pattern);
+
+                       if (!match) return null;
+
+                       const [, action = '', ouidata, subtype, content = ''] = match;
+
+                       if (!validateHexBytes(ouidata, 3) || (content && !validateHexBytes(content, content.split(',').length))) {
+                               return null;
+                       }
+
+                       return { action, ouidata, subtype, content };
+               }
+
+               function load_custom_tlv(section_id, part) {
+                       const tlv = uci.get('lldpd', section_id, 'tlv')?.trim().toLowerCase();
+                       if (!tlv) return '';
+
+                       const parsed = parse_custom_tlv_string(tlv);
+                       if (!parsed) return '';
+
+                       switch (part) {
+                       case 'cust_act': return parsed.action;
+                       case 'cust_oui': return parsed.ouidata;
+                       case 'cust_typ': return parsed.subtype;
+                       case 'cust_inf': return parsed.content;
+                       default: return tlv;
+                       }
+               }
+
+               function write_custom_tlv(section_id) {
+                       const formValue = (part) => this.section.formvalue(section_id, part) || '';
+
+                       const action = formValue('_cust_act');
+                       const ouidata = formValue('_cust_oui');
+                       const subtype = formValue('_cust_typ');
+                       const content = formValue('_cust_inf');
+
+                       const tlvParts = [
+                               action,
+                               ouidata && `oui ${ouidata}`,
+                               subtype && `subtype ${subtype}`,
+                               content && `oui-info ${content}`
+                       ].filter(Boolean).join(' ');
+
+                       uci.set(this.config, section_id, 'tlv', tlvParts);
+               } 
+
+               function intToHexStr(int, uc) {
+                       // return 00-FF for 0-255 int
+                       const str = int.toString(16).padStart(2, '0');
+                       return uc ? str.toUpperCase() : str;
+               } 
+
+               function validate_custom_tlv_string_format(section_id, input) {
+                       if (!input) return true;
+                       return parse_custom_tlv_string(input) ? true : _('Invalid TLV string format');
+               }
+
+               // action
+               oo = ss.option(form.ListValue, '_cust_act', _('Behaviour'));
+               oo.value('', _('default'));
+               oo.value('add', _('add'));
+               oo.value('replace', _('replace'));
+               oo.rmempty = true;
+               oo.write = write_custom_tlv;
+               oo.load = function(section_id) {
+                       return load_custom_tlv(section_id, 'cust_act');
+               };
+
+               // OUI
+               oo = ss.option(form.TextValue, '_cust_oui', _('OUI CSV'));
+               oo.validate = function(section_id, value) {
+                       if (!validateHexBytes(value, 3))
+                               return _('CSV of 3 hex values, e.g. aa,ee,ff');
+                       return true;
+               };
+               oo.rmempty = false;
+               oo.width = 150;
+               oo.optional = false;
+               oo.placeholder = '0f,22,4b';
+               oo.write = write_custom_tlv;
+               oo.load = function(section_id) {
+                       return load_custom_tlv(section_id, 'cust_oui');
+               };
+
+               // sub-type
+               oo = ss.option(form.ListValue, '_cust_typ', _('Subtype'));
+               const typchoices = Array.from({length: 256}, (_, index) => [index, index + " ("+intToHexStr(index, true)+")"] );
+               typchoices.forEach(v => {
+                       oo.value(v[0], v[1]);
+               })
+               oo.rmempty = false;
+               oo.optional = false;
+               oo.write = write_custom_tlv;
+               oo.load = function(section_id) {
+                       return load_custom_tlv(section_id, 'cust_typ');
+               };
+
+               // content
+               oo = ss.option(form.TextValue, '_cust_inf', _('Content'));
+               oo.validate = function(section_id, value) {
+                       if (!value) return true;
+                       if (!validateHexBytes(value, value.split(',').length))
+                               return _('CSV of 1 or more hex values, e.g. aa or bb,cc or 11,55,1a');
+                       return true;
+               };
+               oo.rmempty = true;
+               oo.optional = true;
+               oo.placeholder = 'ff,00,33';
+               oo.write = write_custom_tlv;
+               oo.load = function(section_id) {
+                       return load_custom_tlv(section_id, 'cust_inf');
+               };
+
+               // raw
+               oo = ss.option(form.TextValue, '_raw', _('Raw'));
+               oo.write = function(section_id, value) {
+                       uci.set(this.config, section_id, 'tlv', value);
+               };
+               oo.load = function(section_id) {
+                       return load_custom_tlv(section_id);
+               };
+               oo.validate = validate_custom_tlv_string_format;
+       },
+
        /** @private */
        populateOptions: function(s, data) {
                var o;
@@ -838,6 +999,9 @@ return L.view.extend({
                s.tab('ifaces', _('Network Interfaces'));
                this.populateIfacesOptions(s, 'ifaces', data);
 
+               s.tab('tlvtab', _('TLV'));
+               this.populateExtraTLVOptions(s, 'tlvtab', data);
+
                s.tab('advanced', _('Advanced Settings'));
                this.populateAdvancedOptions(s, 'advanced', data);